# This file is part of Neuroinfo Toolkit.
#
# Neuroinfo Toolkit is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Neuroinfo Toolkit is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Neuroinfo Toolkit.  If not, see <http://www.gnu.org/licenses/>.

from lxml import etree
from neuro.exceptions import IllegalArgumentException
from neuro.exceptions import IOException
from neuro.exceptions import BaseException
import neuro.filesystem as filesystem
from neuro.models.resource import Resource

class XML(Resource):
	'''
	XML
	'''

	def __init__(self, input):
		'''
		Constructor ::
			
			>>> from neuro.models.xml import XML
			>>> xml = XML("<foo />")
			>>> xml = XML("/path/to/file.xml")
			
		:param input: XML string or file name
		:type input: str
		'''
		if(not isinstance(input, basestring)):
			raise IllegalArgumentException("Input must be an instance of str")
		
		input = input.strip()

		if(input == ""):
			raise IllegalArgumentException("Input cannot be empty")

		self._xml = None

		if(input[0] == "<"):
			self._importBlob(input)
		else:
			self._importFile(input)
		
		## --- after import, execute any xincludes
		self._xml.getroottree().xinclude()

	def _importBlob(self, blob):
		'''
		Import a string
		
		:param blob: XML string
		:type blob: str
		:raises: :class:`MalformedXMLException`
		'''
		## --- input validation
		if(not isinstance(blob, basestring)):
			raise IllegalArgumentException("XML blob must be an instance of str")
		elif(blob == ""):
			raise IllegalArgumentException("XML blob cannot be empty")

		try:
			self._xml = etree.XML(blob)
		except Exception, e:
			raise MalformedXMLException(blob, e.__str__())

	def _importFile(self, filename):
		'''
		Import XML from file

		:param filename: File name
		:type filename: str
		:raises: :class:`~neuro.exceptions.IOException`,
				 :class:`MalformedXMLException`
		'''
		filename = filesystem.canonical(filename)

		if(not filesystem.isReadable(filename)):
			raise IOException(IOException.READ, filename)

		self._importBlob(filesystem.fileGetContents(filename))

	def toXML(self):
		'''
		Return XML as lxml.Element object ::
		
			>>> xml.toXML()
			<Element foo at 0x17cc730>
		
		:returns: lxml Element
		:rtype: lxml.Element
		'''
		return self._xml

	def toString(self, pretty=True):
		'''
		Retrieve XML as string ::
		
			>>> xml.toString()
			'<foo/>\\n'
			
		:param pretty: Pretty-print XML
		:type pretty: bool
		:rtype: str
		'''
		if(pretty):
			return etree.tostring(self._xml, pretty_print=True)
		else:
			return self._xml.__str__().strip()

	def xpath(self, query, namespaces={}):
		'''
		Execute XPath query ::
		
			>>> xml.xpath("/foo[@bar='biz']")
			<Element foo at 0x17cc730>
			
		:param query: XPath query
		:type query: str
		:rtype: str, list, lxml.Element
		:raises: :class:`~MissingFieldException`
		'''
		## --- input validation
		if(not isinstance(query, basestring)):
			raise IllegalArgumentException("XPath query must be an instance of str")
		elif(not isinstance(namespaces, dict)):
				raise IllegalArgumentException("Namespaces must be an instance of dict")
		
		query = query.strip()

		if(query == ""):
			raise IllegalArgumentException("XPath query cannot be null")

		## --- execute the query
		try:
			result = self._xml.xpath(query, namespaces=namespaces)
		except Exception, e:
			raise XPathException(self, query, e.__str__())

		if(len(result) == 0):
			return None
		elif(len(result) == 1):
			return result[0]
		else:
			return result

	def applyXSLT(self, xslt, params=None, in_place=True):
		'''
		Transform this document with an XSL template ::
		
			>>> xslt = XML("/path/to/file.xsl")
			>>> xml.applyXSLT(xslt)

		:param xslt: XSLT :class:`XML` object
		:type xslt: :class:`XML`
		:param params: XSLT parameters
		:type params: dict
		:param in_place: Modify XML document in-place, or return a new document (not implemented)
		:type in_place: bool
		:rtype: :class:`XML`
		:raises: :class:`~neuro.exceptions.MissingFieldException`,
				 :class:`XSLTException`	
		'''
		if(not isinstance(xslt, XML)):
			raise IllegalArgumentException("XSLT must be an instance of XML")

		hasParams = False

		if(params != None):
			hasParams = True

			if(not isinstance(params, dict)):
				raise IllegalArgumentException("If defined, parameters must be an instance of dict")
			elif(len(params) == 0):
				hasParams = False

		transformer = etree.XSLT(xslt.toXML())

		try:
			if(hasParams):
				self._xml = transformer(self._xml, **params)
			else:
				self._xml = transformer(self._xml)
		except Exception, e:
			raise XSLTException(self, xslt, e.__str__())

	def validate(self, xml, throw=False):
		'''
		Check if XML is valid against XML schema ::
		
			>>> schema = XML("/path/to/file.xsd")
			>>> xml.validate(xml)
			True
			
		:param xml: XML schema :class:`XML` object
		:type xml: :class:`XML`
		:param throw: Do you want an exception, or just a boolean
		:type throw: bool
		:rtype: bool
		:raises: :class:`XMLSchemaException`
		'''
		if(not isinstance(xml, XML)):
			raise IllegalArgumentException("Input must be an instance of XML")

		try:
			schema = etree.XMLSchema(self._xml)

			if(throw):
				result = schema.assertValid(xml.toXML())
			else:
				result = schema.validate(xml.toXML())
		except Exception, e:
			raise XMLSchemaException(e.__str__())

		return result

class MalformedXMLException(BaseException):
	'''
	Malformed XML exception
	'''

	def __init__(self, xml, desc=""):
		'''
		Constructor

		:param xml: XML string
		:type xml: str
		:param desc: Description
		:type desc: str
		'''
		BaseException.__init__(self)

		if(not isinstance(xml, basestring)):
			raise IllegalArgumentException("XML must be an instance of str")
		elif(not isinstance(desc, basestring)):
			raise IllegalArgumentException("Description must be an instance of str")

		xml = xml.strip()
		desc = desc.strip()

		if(xml == ""):
			raise IllegalArgumentException("XMl must be an instance of str")

		self._xml = xml
		self._desc = desc

	def getXML(self):
		'''
		Get XML that triggered this exception

		:rtype: str
		'''
		return self._xml

	def getDescription(self):
		'''
		Get exception description

		:rtype: str
		'''
		return self._desc
	
	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		message = "Malformed XML: " + self._xml
		
		if(self._desc != ""):
			message += "\n\nDescription: " + self._desc

		return message

class XMLSchemaException(BaseException):
	'''
	XML Schema validation exception
	'''

	def __init__(self, xml, schema, desc=""):
		'''
		Constructor

		:param xml: :class:`XML` object
		:type xml: :class:`XML`
		:param schema: XML schema object
		:type schema: :class:`XML`
		:param desc: Description
		:type desc: str
		'''
		BaseException.__init__(self)

		if(not isinstance(xml, XML)):
			raise IllegalArgumentException("Input XML must be an instance of XML")
		elif(not isinstance(schema, XML)):
			raise IllegalArgumentException("Input XML Schema must be an instance of XML")
		elif(not isinstance(desc, basestring)):
			raise IllegalArgumentException("Description must be an instance of str")

		desc = desc.strip()
		
		self._xml = xml
		self._schema = schema
		self._desc = desc

	def getXML(self):
		'''
		Get XML that triggered this exception

		:rtype: :class:`XML`
		'''
		return self._xml

	def getSchema(self):
		'''
		Get XML Schema that triggered this exception

		:rtype: :class:`XML`
		'''
		return self._schema

	def getDescription(self):
		'''
		Get exception description

		:rtype: str
		'''
		return self._desc

	def getMessage(self):
		'''
		Get custom message

		:rtype: str
		'''
		return "XML is invalid: " + self._desc

class XSLTException(BaseException):
	'''
	XSLT transformation exception
	'''

	def __init__(self, xml, xslt, desc=""):
		'''
		Constructor

		:param xml: :class:`XML` object
		:type xml: :class:`XML`
		:param xslt: XML schema object
		:type xslt: :class:`XML`
		:param desc: Description
		:type desc: str
		'''
		BaseException.__init__(self)

		if(not isinstance(xml, XML)):
			raise IllegalArgumentException("Input XML must be an instance of XML")
		elif(not isinstance(xslt, XML)):
			raise IllegalArgumentException("Input XSLT must be an instance of XML")
		elif(not isinstance(desc, basestring)):
			raise IllegalArgumentException("Description must be an instance of str")

		desc = desc.strip()

		self._xml = xml
		self._xslt = xslt
		self._desc = desc

	def getXML(self):
		'''
		Get XML that triggered this exception

		:rtype: :class:`XML`
		'''
		return self._xml

	def getXSLT(self):
		'''
		Get XSLT that triggered this exception

		:rtype: :class:`XML`
		'''
		return self._xslt

	def getDescription(self):
		'''
		Get the exception description

		:rtype: str
		'''
		return self._desc

	def getMessage(self):
		'''
		Get custom message
		'''
		return "XSLT transformation failed: " + self._desc

class XPathException(BaseException):
	'''
	XPath exception
	'''

	def __init__(self, xml, xpath, desc=""):
		'''
		Constructor

		:param xml: :class:`XML` object
		:type xml: :class:`XML`
		:param xpath: XPath expression
		:type xpath: str
		:param desc: Description
		:type desc: str
		'''
		BaseException.__init__(self)
		
		if(not isinstance(xml, XML)):
			raise IllegalArgumentException("Input XML must be an instance of XML")
		elif(not isinstance(xpath, basestring)):
			raise IllegalArgumentException("XPath must be an instance of str")
		elif(not isinstance(desc, basestring)):
			raise IllegalArgumentException("Description must be an instance of str")
		
		xpath = xpath.strip()
		desc = desc.strip()
		
		self._xml = xml
		self._xpath = xpath
		self._desc = desc
		
	def getXML(self):
		'''
		Get XML that triggered this exception
		
		:rtype: :class:`XML`
		'''
		return self._xml
	
	def getXPath(self):
		'''
		Get XPath that triggered this exception
		
		:rtype: str
		'''
		return self._xpath
	
	def getDescription(self):
		'''
		Get a description of this exception
		
		:rtype: str
		'''
		return self._desc
	
	def getMessage(self):
		'''
		Get custom message
		
		:rtype: str
		'''
		return "XPath error: " + self._desc
